package jp.co.nes.awssupport.compile.batch;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.YearMonth;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.GZIPOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import org.apache.commons.lang3.StringUtils;
import org.kohsuke.args4j.Option;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.amazonaws.services.s3.model.ObjectListing;
import com.amazonaws.services.s3.model.S3ObjectSummary;

import jp.co.nes.awssupport.business.bean.BillingCsvBean;
import jp.co.nes.awssupport.business.bean.CommonContainerBean;
import jp.co.nes.awssupport.business.bean.CommonContainerBean.ProcessingResult;
import jp.co.nes.awssupport.business.bean.CommonInfoBean;
import jp.co.nes.awssupport.business.bean.ManifestBean;
import jp.co.nes.awssupport.business.constant.LineItemType;
import jp.co.nes.awssupport.business.logic.BillingCsvReader;
import jp.co.nes.awssupport.business.logic.CompileProgressManage;
import jp.co.nes.awssupport.business.utility.ManifestHelper;
import jp.co.nes.awssupport.common.base.AbstractActivityTaskClient;
import jp.co.nes.awssupport.common.base.CommonException;
import jp.co.nes.awssupport.common.bean.AppInfoBean;
import jp.co.nes.awssupport.common.utility.AppInfoUtil;
import jp.co.nes.awssupport.common.utility.AwsKmsHelper;
import jp.co.nes.awssupport.common.utility.AwsS3Helper;
import jp.co.nes.awssupport.common.utility.BeanUtil;
import jp.co.nes.awssupport.common.utility.DateUtil;
import jp.co.nes.awssupport.common.utility.JsonUtil;

/**
 * 請求CSV分割です。
 */
public class BillingCsvSplit extends AbstractActivityTaskClient<CommonInfoBean, CommonContainerBean<CommonInfoBean>> {

    /** ロガー */
    private static Logger logger = LoggerFactory.getLogger(BillingCsvSplit.class);

    /** アクティビティのarn(コマンドライン引数で設定) */
    @Option(name = "-aarn", aliases = "--activityArn", usage = "アクティビティのarn", metaVar = "activityArn", required = true)
    private String activityArn;

    /**
     * メインメソッドです。
     *
     * @param args コマンドライン引数
     */
    public static void main(String[] args) {
        BillingCsvSplit client = new BillingCsvSplit();
        client.execute(args);
    }

    /**
     * デフォルトコンストラクタです。
     */
    public BillingCsvSplit() {
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.common.base.AbstractStepFunctionsCallBatchClient#getActivityArn()
     */
    @Override
    protected String getActivityArn() {
        return activityArn;
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.common.base.AbstractStepFunctionsCallBatchClient#getInputClass()
     */
    @Override
    protected Class<CommonInfoBean> getInputClass() {
        return CommonInfoBean.class;
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.common.base.AbstractStepFunctionsCallBatchClient#execute(java.lang.Object)
     */
    @Override
    protected CommonContainerBean<CommonInfoBean> runActivityTask(CommonInfoBean input) {
        String category = "BillingCsv";
        String localWorkRootDir = AppInfoUtil.getValAndEmptyChk(category, "LocalWorkRootDir");
        String manifestFileName = AppInfoUtil.getVal(category, "ManifestFileName", "billing-csv-Manifest.json");

        String targetYearMonthValue = input.getTargetYearMonthValue();
        YearMonth targetYearMonth = DateUtil.toYearMonth(targetYearMonthValue);
        localWorkRootDir = String.format(localWorkRootDir, targetYearMonthValue);

        if (logger.isDebugEnabled()) {
            logger.debug(category + ",LocalWorkRootDir(after formatting)=" + localWorkRootDir);
            logger.debug(category + ",ManifestFileName=" + manifestFileName);
        }

        Map<String, OutputStream> outputStreamMap = new HashMap<>();

        clearLocalWork(localWorkRootDir);
        deleteObjects(targetYearMonthValue);

        List<String> outputIdList = new ArrayList<>();
        List<String> refundLineItemList = new ArrayList<>();
        List<String> nullLineItemTypeIdList = new ArrayList<>();
        List<String> noCompileTargetIdList = new ArrayList<>();
        CompileProgressManage cpManage = new CompileProgressManage(targetYearMonth);
        List<String> cpManageIdList = cpManage.getLinkedAccountIdList();

        int invoiceIdStatus = 0; // 0: 初期状態, 1: 空白が存在する, 2: 空白が存在しない
        String categoryPayer = "Payer";
        List<AppInfoBean> payerAppInfoList = AppInfoUtil.getList(categoryPayer);
        for (AppInfoBean payerAppInfo : payerAppInfoList) {
            Map<String, String> info = payerAppInfo.getInfo();
            String payerAccessKey = getPayerAccessKey(info);
            String payerSecretKey = getPayerSecretKey(info);
            String payerBucketName = getPayerBucketName(info);
            String payerManifestObjectKey = getPayerManifestObjectKey(info, targetYearMonth);
            String payerAccountId = getPayerAccountId(info);

            if (logger.isDebugEnabled()) {
                logger.debug(categoryPayer + ",BucketName=" + payerBucketName);
                logger.debug(categoryPayer + ",ManifestObjectKey(after formatting)=" + payerManifestObjectKey);
                logger.debug(categoryPayer + ",AccountId=" + payerAccountId);
            }

            // 請求CSV分割
            logger.info("runActivityTask: billing csv read and write start");
            ManifestBean payerManifestBean = getManifestBean(payerBucketName, payerManifestObjectKey, payerAccessKey,
                    payerSecretKey);
            String workDir = localWorkRootDir + File.separator + payerAccountId;
            createLocalWorkDir(workDir);
            File manifestFile = Paths.get(workDir, manifestFileName).toFile();
            BillingCsvReader reader = null;
            try (BufferedWriter writer = new BufferedWriter(new FileWriter(manifestFile))) {
                JsonUtil.writeValue(writer, payerManifestBean);

                String header = null;
                List<String> payerObjectKeyList = payerManifestBean.getReportKeys();
                for (String payerObjectKey : payerObjectKeyList) {
                    reader = createBillingCsvReader(payerManifestBean, payerBucketName, payerObjectKey, payerAccessKey,
                            payerSecretKey);
                    while (reader.readBean()) {
                        if (reader.isHeader()) {
                            // ファイルの1行目の場合
                            if (header == null) {
                                header = reader.getHeader();
                            }
                            continue;
                        }

                        BillingCsvBean bean = reader.getBean();

                        invoiceIdStatus = getInvoiceIdStatus(invoiceIdStatus, bean);

                        String linkedAccountId = bean.getLinkedAccountId();

                        if (noCompileTargetIdList.stream().anyMatch(id -> id.equals(linkedAccountId))) {
                            // 連結アカウントIDがnoCompileTargetIdListに含まれていた場合、
                            // すでにスキップした連結アカウントIDなのでcontinueする。
                            continue;
                        }
                        if (!cpManageIdList.stream().anyMatch(id -> id.equals(linkedAccountId))) {
                            // 集計進捗状況にない(契約情報では集計対象になっていない)連結アカウントの場合
                            // -> 在庫だと想定されるデータ
                            noCompileTargetIdList.add(linkedAccountId);
                            continue;
                        }

                        OutputStream outputStream = outputStreamMap.get(payerAccountId + linkedAccountId);
                        if (outputStream == null) {
                            outputStream = createOutputStream(payerObjectKey, workDir, payerManifestBean,
                                    linkedAccountId);

                            outputStreamMap.put(payerAccountId + linkedAccountId, outputStream);

                            outputStream.write(header.getBytes());
                            outputStream.write('\n');

                            if (!outputIdList.stream().anyMatch(id -> id.equals(linkedAccountId))) {
                                outputIdList.add(linkedAccountId);
                            }
                        }

                        if (reader.isInvalidLine()) {
                            // 無効行の場合
                            continue;
                        }

                        if (bean.getLineItemType() == null) {
                            if (!nullLineItemTypeIdList.stream().anyMatch(id -> id.equals(linkedAccountId))) {
                                nullLineItemTypeIdList.add(linkedAccountId);
                            }
                        } else if (bean.getLineItemType() == LineItemType.REFUND) {
                            String addValue = createAddValue(bean);
                            if (!refundLineItemList.stream().anyMatch(value -> value.equals(addValue))) {
                                refundLineItemList.add(addValue);
                            }
                        }

                        outputStream.write(reader.getLine().getBytes());
                        outputStream.write('\n');
                    }
                    close(reader);
                    reader = null;
                }
            } catch (IOException e) {
                throw new CommonException(e);
            } finally {
                close(reader);
                close(outputStreamMap);
            }
            logger.info("runActivityTask: billing csv read and write end");

            logger.info("runActivityTask: s3 upload start");
            putObjects(workDir, payerManifestBean, manifestFile, targetYearMonthValue, payerAccountId);
            logger.info("runActivityTask: s3 upload end");
        }

        if (invoiceIdStatus == 0) {
            // 全ての一括請求アカウントの請求CSVが、存在しない or ヘッダのみだった場合
            logger.info("runActivityTask: billing csv does not exist or header only");
        } else if (invoiceIdStatus == 1) {
            // 全ての一括請求アカウントの請求CSVのいずれかに、空白が存在した場合(請求CSV未確定)
            logger.info("runActivityTask: billing csv has not been confirmed");
        } else if (invoiceIdStatus == 2) {
            // 全ての一括請求アカウントの請求CSVで、空白が存在しない場合(請求CSV確定)
            logger.info("runActivityTask: billing csv is confirmed");
            cpManage.confirmedBillingCsv();
        } else {
            // 通常発生しない。
            throw new CommonException("invoiceIdStatus is wrong");
        }

        List<String> csvNotExistIdList = new ArrayList<>();
        for (String linkedAccountId : cpManageIdList) {
            if (!outputIdList.stream().anyMatch(id -> id.equals(linkedAccountId))) {
                // 集計進捗状況にある(契約情報では集計対象になっている)が、請求CSVに対象の連結アカウントIDが存在しなかった場合、
                // 集計進捗状況テーブルから情報を削除する。
                // -> 別Payerの請求CSVに含まれているアカウントと想定されるが、本プログラムで使用するPayer関係の設定が適切でない。
                //    契約情報の内容が誤っている。
                //    等々
                csvNotExistIdList.add(linkedAccountId);
                cpManage.delete(linkedAccountId);
            }
        }

        // Refund行に関する情報をワーニングログに出力する。
        warnLogForRefund(refundLineItemList);

        // 有効行で、LineItemTypeがnullのものが存在した連結アカウントについてワーニングログを出力する。
        String preNullLineItemTypeIdValue = "Null LineItemType exist";
        warnLogForWrongLinkedAccountId(nullLineItemTypeIdList, preNullLineItemTypeIdValue);

        // 集計進捗状況にない(契約情報では集計対象になっていない)連結アカウントについてワーニングログを出力する。
        String preNoCompileTargetIdValue = "No compile target, BillingCsv exist";
        warnLogForWrongLinkedAccountId(noCompileTargetIdList, preNoCompileTargetIdValue);

        // 集計進捗状況にある(契約情報では集計対象になっている)が、請求CSVに対象の連結アカウントIDが
        // 存在しないものについてワーニングログを出力する。
        String preCsvNotExistIdValue = "Compile target, BillingCsv not exist";
        warnLogForWrongLinkedAccountId(csvNotExistIdList, preCsvNotExistIdValue);

        // 返却内容の作成
        CommonInfoBean info = new CommonInfoBean();
        BeanUtil.copyProperties(info, input);
        CommonContainerBean<CommonInfoBean> output = new CommonContainerBean<>();
        output.setInfoBean(info);
        output.setProcessingResult(ProcessingResult.SUCCESS);
        return output;
    }

    /**
     * 請求CSVを読み込み、分割したファイルを出力するディレクトリのクリア処理です。
     *
     * @param localWorkDir 請求CSVを分割したファイルを出力するディレクトリ
     */
    private void clearLocalWork(String localWorkDir) {
        Path dir = Paths.get(localWorkDir);
        try {
            if (Files.exists(dir)) {
                // ファイル出力先のルートディレクトリが作成済みの場合
                Files.list(dir).forEach(path -> {
                    try {
                        if (Files.isDirectory(path)) {
                            // ディレクトリの場合
                            String value = localWorkDir + File.separator + path.getFileName();
                            clearLocalWork(value);
                            Files.delete(path);
                        } else {
                            // ディレクトリ以外の場合
                            Files.delete(path);
                        }
                    } catch (IOException e) {
                        throw new CommonException(e);
                    }
                });
            }
        } catch (IOException e) {
            throw new CommonException(e);
        }
    }

    /**
     * 請求CSVを読み込み、分割したファイルを出力するディレクトリを作成します。
     *
     * @param localWorkDir 請求CSVを分割したファイルを出力するディレクトリ
     */
    private void createLocalWorkDir(String localWorkDir) {
        Path dir = Paths.get(localWorkDir);
        try {
            if (!Files.exists(dir)) {
                // ファイル出力先のルートディレクトリが未作成の場合
                Files.createDirectories(dir);
            }
        } catch (IOException e) {
            throw new CommonException(e);
        }
    }

    /**
     * 読み込む請求CSVに関する一括請求アカウントのアクセスキーを返却します。
     *
     * @param info アプリケーション情報の情報マップ
     * @return 一括請求アカウントのアクセスキー
     */
    private String getPayerAccessKey(Map<String, String> info) {
        String payerAccessKey = null;
        String encryptPayerAccessKey = info.get("EncryptAccessKey");
        if (encryptPayerAccessKey == null) {
            payerAccessKey = info.get("AccessKey");
        } else {
            logger.info("getPayerAccessKey: create helper start");
            AwsKmsHelper helper = createAwsKmsHelper();
            payerAccessKey = helper.decrypt(encryptPayerAccessKey);
            logger.info("getPayerAccessKey: create helper end");
        }
        return payerAccessKey;
    }

    /**
     * 読み込む請求CSVに関する一括請求アカウントのシークレットキーを返却します。
     *
     * @param info アプリケーション情報の情報マップ
     * @return 一括請求アカウントのシークレットキー
     */
    private String getPayerSecretKey(Map<String, String> info) {
        String payerSecretKey = null;
        String encryptPayerSecretKey = info.get("EncryptSecretKey");
        if (encryptPayerSecretKey == null) {
            payerSecretKey = info.get("SecretKey");
        } else {
            logger.info("getPayerSecretKey: create helper start");
            AwsKmsHelper helper = createAwsKmsHelper();
            payerSecretKey = helper.decrypt(encryptPayerSecretKey);
            logger.info("getPayerSecretKey: create helper end");
        }
        return payerSecretKey;
    }

    /**
     * 読み込む請求CSVに関するバケット名を返却します。
     *
     * @param info アプリケーション情報の情報マップ
     * @return 読み込む請求CSVに関するバケット名
     */
    private String getPayerBucketName(Map<String, String> info) {
        String payerBucketName = info.get("BucketName");
        if (StringUtils.isEmpty(payerBucketName)) {
            throw new CommonException("payerBucketName is null or empty");
        }
        return payerBucketName;
    }

    /**
     * 読み込む請求CSVに関するマニュフェストオブジェクトキーを返却します。
     *
     * @param info アプリケーション情報の情報マップ
     * @param targetYearMonth 処理対象年月
     * @return 読み込む請求CSVに関するマニュフェストオブジェクトキー
     */
    private String getPayerManifestObjectKey(Map<String, String> info, YearMonth targetYearMonth) {
        String targetYearMonthValue = DateUtil.toString(targetYearMonth);
        YearMonth nextYearMonth = targetYearMonth.plusMonths(1);
        String nextYearMonthValue = DateUtil.toString(nextYearMonth);

        String payerManifestObjectKey = info.get("ManifestObjectKey");
        if (StringUtils.isEmpty(payerManifestObjectKey)) {
            throw new CommonException("payerManifestObjectKey is null or empty");
        }
        payerManifestObjectKey = String.format(payerManifestObjectKey, targetYearMonthValue, nextYearMonthValue);
        return payerManifestObjectKey;
    }

    /**
     * 読み込む請求CSVに関する一括請求アカウントIDを返却します。
     *
     * @param info アプリケーション情報の情報マップ
     * @return 一括請求アカウントID
     */
    private String getPayerAccountId(Map<String, String> info) {
        String payerAccountId = info.get("AccountId");
        if (StringUtils.isEmpty(payerAccountId)) {
            throw new CommonException("payerAccountId is null or empty");
        }
        return payerAccountId;
    }

    /**
     * 請求CSVのマニュフェストを返却します。<br>
     * バケット名、オブジェクトキーには、マニュフェストのバケット名、オブジェクトキーを設定します。<br>
     * アクセスキー、シークレットキーには、読み込むファイルの接続先を設定します。<br>
     * アクセスキーがnullの場合は、デフォルトの接続先になります。
     *
     * @param bucketName バケット名
     * @param objectKey オブジェクトキー
     * @param accessKey アクセスキー
     * @param secretKey シークレットキー
     * @return 請求CSVのマニュフェスト
     */
    protected ManifestBean getManifestBean(String bucketName, String objectKey, String accessKey, String secretKey) {
        ManifestHelper manifestHelper = new ManifestHelper(bucketName, objectKey, accessKey, secretKey);
        return manifestHelper.getManifestBean();
    }

    /**
     * 請求CSVReaderを生成します。<br>
     * マニュフェスト、バケット名、オブジェクトキーには、読み込むファイルのマニュフェスト、バケット名、
     * オブジェクトキーを設定します。<br>
     * アクセスキー、シークレットキーには、読み込むファイルの接続先を設定します。<br>
     * アクセスキーがnullの場合は、デフォルトの接続先になります。
     *
     * @param manifestBean マニュフェスト
     * @param bucketName バケット名
     * @param objectKey オブジェクトキー
     * @param accessKey アクセスキー
     * @param secretKey シークレットキー
     * @return 請求CSVReader
     */
    protected BillingCsvReader createBillingCsvReader(ManifestBean manifestBean, String bucketName, String objectKey,
            String accessKey, String secretKey) {
        return new BillingCsvReader(manifestBean, bucketName, objectKey, accessKey, secretKey);
    }

    /**
     * kmsヘルパーを生成します。
     *
     * @return kmsヘルパー
     */
    protected AwsKmsHelper createAwsKmsHelper() {
        return new AwsKmsHelper();
    }

    /**
     * InvoiceId状態と請求CSVの内容を確認し、
     * InvoiceId状態InvoiceId状態(0: 初期状態, 1: 空白が存在する, 2: 空白が存在しない)を返却します。
     *
     * @param invoiceIdStatus InvoiceId状態
     * @param bean 請求CSV
     * @return InvoiceId状態
     */
    private int getInvoiceIdStatus(int invoiceIdStatus, BillingCsvBean bean) {
        int result = invoiceIdStatus;
        if (invoiceIdStatus == 0) {
            if (StringUtils.isEmpty(bean.getInvoiceId())) {
                result = 1;
            } else {
                result = 2;
            }
        } else if (invoiceIdStatus == 2) {
            if (StringUtils.isEmpty(bean.getInvoiceId())) {
                result = 1;
            }
        }
        return result;
    }

    /**
     * 出力ストリームを作成します。
     *
     * @param payerObjectKey Payerオブジェクトキー
     * @param workDir 請求CSVを分割したファイルを出力するディレクトリ
     * @param manifestBean マニュフェスト
     * @param linkedAccountId 連結アカウントID
     * @return 出力ストリーム
     * @throws IOException IO例外
     */
    private OutputStream createOutputStream(String payerObjectKey, String workDir, ManifestBean manifestBean,
            String linkedAccountId) throws IOException {
        String compression = manifestBean.getCompression();
        if (StringUtils.isEmpty(compression)) {
            throw new CommonException("compression is null or empty");
        }

        OutputStream outputStream = null;
        if (compression.toLowerCase().equals("zip")) {
            File file = Paths.get(workDir, linkedAccountId + "." + compression.toLowerCase()).toFile();
            ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(file));
            zip.putNextEntry(new ZipEntry(linkedAccountId + ".csv"));
            outputStream = new BufferedOutputStream(zip);
        } else if (compression.toLowerCase().equals("gzip")) {
            int lastIndex = payerObjectKey.lastIndexOf(".");
            if (lastIndex < 0) {
                StringBuilder builder = new StringBuilder();
                builder.append("payerObjectKey is wrong(payerObjectKey=");
                builder.append(payerObjectKey);
                builder.append(")");
                throw new CommonException(builder.toString());
            }
            String extension = payerObjectKey.substring(lastIndex + 1);
            File file = Paths.get(workDir, linkedAccountId + ".csv." + extension).toFile();
            GZIPOutputStream gzip = new GZIPOutputStream(new FileOutputStream(file));
            outputStream = new BufferedOutputStream(gzip);
        } else {
            throw new CommonException("compression wrong(compression=" + compression + ")");
        }
        return outputStream;
    }

    /**
     * リストに登録する文字列を作成し、返却します。
     *
     * @param bean 請求CSV
     * @return リストに登録する文字列
     */
    private String createAddValue(BillingCsvBean bean) {
        StringBuilder builder = new StringBuilder();
        builder.append(bean.getLinkedAccountId());
        builder.append(":");
        builder.append(bean.getProductName());
        builder.append(":");
        builder.append(bean.getUsageType());
        builder.append(":");
        builder.append(bean.getUsageQuantity());
        return builder.toString();
    }

    /**
     * Readerをクローズします。
     *
     * @param reader Reader
     */
    private void close(BillingCsvReader reader) {
        try {
            if (reader != null) {
                reader.close();
            }
        } catch (IOException e) {
            // ワーニングログを出力し、処理を続行する。
            logger.warn(e.getMessage(), e);
        }
    }

    /**
     * Mapに含まれる出力ストリームをクローズします。
     *
     * @param outputStreamMap 出力ストリームが含まれているMap
     */
    private void close(Map<String, OutputStream> outputStreamMap) {
        outputStreamMap.forEach((linkedAccountId, outputStream) -> {
            try {
                outputStream.close();
            } catch (IOException e) {
                // ワーニングログを出力し、処理を続行する。
                logger.warn(e.getMessage() + "(linkedAccountId=" + linkedAccountId + ")", e);
            }
        });
        outputStreamMap.clear();
    }

    /**
     * s3にある請求CSVを分割したファイルを削除します。
     *
     * @param targetYearMonthValue 処理対象年月文字列
     */
    private void deleteObjects(String targetYearMonthValue) {
        String category = "BillingCsv";
        String bucketName = AppInfoUtil.getValAndEmptyChk(category, "BucketName");

        if (logger.isDebugEnabled()) {
            logger.debug(category + ",BucketName=" + bucketName);
        }

        logger.info("putObjects: s3 get list start");
        AwsS3Helper helperForObjectListing = createAwsS3Helper();
        ObjectListing objectListing = helperForObjectListing.listObjects(bucketName, targetYearMonthValue);
        logger.info("putObjects: s3 get list end");

        logger.info("putObjects: s3 delete object start");
        List<S3ObjectSummary> s3ObjectSummaryList = objectListing.getObjectSummaries();
        s3ObjectSummaryList.stream().parallel().forEach(s3ObjectSummary -> {
            // parallelでs3にある請求CSVを分割したファイルを削除する。
            AwsS3Helper helper = createAwsS3Helper();
            helper.deleteObject(bucketName, s3ObjectSummary.getKey());
        });
        logger.info("putObjects: s3 delete object end");
    }

    /**
     * 請求CSVを分割したファイルをs3にアップロードします。
     *
     * @param localWorkDir 請求CSVを分割したファイルを出力するディレクトリ
     * @param manifestBean マニュフェスト
     * @param manifestFile 出力したマニュフェストファイル
     * @param targetYearMonthValue 処理対象年月文字列
     * @param payerAccountId PayerアカウントID
     */
    private void putObjects(String localWorkDir, ManifestBean manifestBean, File manifestFile,
            String targetYearMonthValue, String payerAccountId) {
        String category = "BillingCsv";
        String bucketName = AppInfoUtil.getValAndEmptyChk(category, "BucketName");
        String objectKey = AppInfoUtil.getValAndEmptyChk(category, "ObjectKey");
        String manifestObjectKey = AppInfoUtil.getValAndEmptyChk(category, "ManifestObjectKey");
        String contentType = AppInfoUtil.getVal(category, "ContentType", "application/octet-stream");
        String manifestContentType = AppInfoUtil.getVal(category, "ManifestContentType", "application/json");

        if (logger.isDebugEnabled()) {
            logger.debug(category + ",BucketName=" + bucketName);
            logger.debug(category + ",ObjectKey(before formatting)=" + objectKey);
            logger.debug(category + ",ManifestObjectKey(before formatting)=" + manifestObjectKey);
            logger.debug(category + ",ContentType=" + contentType);
            logger.debug(category + ",ManifestContentType=" + manifestContentType);
        }

        Path dir = Paths.get(localWorkDir);
        try {
            logger.info("putObjects: s3 put object start");
            Files.list(dir).parallel().forEach(file -> {
                try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file.toFile()))) {
                    String fileName = file.getFileName().toString();
                    String linkedAccountId = null;
                    String oKey = null;
                    String encoding = null;
                    String type = null;
                    if (manifestFile.compareTo(file.toFile()) == 0) {
                        // マニュフェストの場合
                        oKey = String.format(manifestObjectKey, targetYearMonthValue, payerAccountId);
                        encoding = "UTF-8";
                        type = manifestContentType;
                    } else {
                        int index = fileName.indexOf(".");
                        if (index < 0) {
                            // 通常発生しない。
                            throw new CommonException("fileName is wrong(fileName=" + fileName + ")");
                        }
                        linkedAccountId = fileName.substring(0, index);
                        oKey = String.format(objectKey, targetYearMonthValue, payerAccountId, linkedAccountId);
                        encoding = manifestBean.getCharset();
                        type = contentType;
                    }
                    long contentLength = Files.size(file);

                    AwsS3Helper helper = createAwsS3Helper();
                    helper.putObject(bucketName, oKey, inputStream, encoding, contentLength, type);
                } catch (IOException e) {
                    throw new CommonException(e);
                }
            });
        } catch (IOException e) {
            throw new CommonException(e);
        }
        logger.info("putObjects: s3 put object end");
    }

    /**
     * s3ヘルパーを生成します。
     *
     * @return s3ヘルパー
     */
    protected AwsS3Helper createAwsS3Helper() {
        return new AwsS3Helper();
    }

    /**
     * 有効行で、LineItemTypeがnullのものが存在した連結アカウント、集計進捗状況にない(契約情報では集計対象になっていない)連結アカウント、
     * 集計進捗状況にある(契約情報では集計対象になっている)が、請求CSVに対象の連結アカウントIDが存在しないものについて、
     * ワーニングログを出力します。
     *
     * @param linkedAccountIdList ログに出力する連結アカウントIDリスト
     * @param preValue ログの先頭に出力する内容
     */
    private void warnLogForWrongLinkedAccountId(List<String> linkedAccountIdList, String preValue) {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < linkedAccountIdList.size(); i++) {
            if (i == 0) {
                builder.append(preValue);
                builder.append(" [linkedAccountId: ");
            }
            builder.append(linkedAccountIdList.get(i));
            if (i == linkedAccountIdList.size() - 1) {
                builder.append("]");
            } else {
                builder.append(",");
            }
        }
        String value = builder.toString();
        if (!StringUtils.isEmpty(value)) {
            logger.warn(builder.toString());
        }
    }

    /**
     * Refund行に関する情報をワーニングログに出力します。
     *
     * @param refundLineItemList Refund行に関する情報リスト
     */
    private void warnLogForRefund(List<String> refundLineItemList) {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < refundLineItemList.size(); i++) {
            if (i == 0) {
                builder.append("Refund Info [linkedAccountId:productName:usageType:usageQuantity=");
            }
            builder.append(refundLineItemList.get(i));
            if (i == refundLineItemList.size() - 1) {
                builder.append("]");
            } else {
                builder.append(",");
            }
        }
        String value = builder.toString();
        if (!StringUtils.isEmpty(value)) {
            logger.warn(builder.toString());
        }
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.common.base.AbstractActivityTaskClient#finish(boolean, java.lang.Throwable)
     */
    @Override
    protected void finish(boolean isSuccess, Throwable throwable) {
        // 終了時、ワークディレクトリに出力している内容をクリアする。
        String category = "BillingCsv";
        String localWorkRootDir = AppInfoUtil.getValAndEmptyChk(category, "LocalWorkRootDir");
        clearLocalWork(localWorkRootDir);
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.common.base.AbstractClient#debugArgument()
     */
    @Override
    protected void debugArgument() {
        if (logger.isDebugEnabled()) {
            logger.debug("activityArn=" + activityArn);
        }
    }
}
